Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FFI callbacks and vtables #1818

Merged
merged 2 commits into from
Jan 25, 2024
Merged

FFI callbacks and vtables #1818

merged 2 commits into from
Jan 25, 2024

Conversation

bendk
Copy link
Contributor

@bendk bendk commented Oct 30, 2023

This PR updates the callback interface code by adding new FfiType variants:

  • FfiType::Callback is a callback function.
  • FfiType::Struct is a repr(C) struct of FfiType fields.
  • FfiType::Reference is a pointer/reference to another FfiType.

FfiType::Callback and FfiType::Struct store a name that corresponds to a callback/struct definition that the foreign bindings render. IOW, we can now define callbacks/structs in the ComponentInterface, then refer to them by name as ffi types.

These types are used to replace the callback interface code. Instead of having the foreign code register a FfiType::ForeignCallback, it now registers a reference to a vtable: a struct where each field is a callback that implements one of the callback interface methods, plus a field for the uniffi_free method.

The primary motivation for this is to prepare async callback methods. I think those methods should input an extra argument that's a callback to call when the async method completes. However, that presents a couple challenges to the current system:

  • That callback would be generic on the FfiType, which is a new thing that could get very messy. This code will allow us to handle the monomorphization in the ComponentInterface code and keep the bindings generators simple.
  • Currently all arguments are passed in a RustBuffer, but I don't want to read a function pointer out of that. For one, it feels like too much transmuting to me and I don't like making too many assumptions about function pointers. Secondly, it seems like it's mixing abstractions. The read operation should be used for Type values, not for FfiType. VTables give us the flexibility to avoid this since we can just add a new FfiType parameter for async methods only.

I think this change will also improve our lives in general. It's currently pretty tedious to add a new callback type or refactor an existing one. I think this system will make that easier with a lot less fiddling around to get the signatures and FFI types correct for each language. Also, the concept of vtables opens up some interesting possibilities and if we had that before the initial async work it could have simplified some things.

@bendk bendk requested a review from a team as a code owner October 30, 2023 14:36
@bendk bendk requested review from jhugman and mhammond and removed request for a team October 30, 2023 14:36
@bendk bendk mentioned this pull request Oct 30, 2023
@bendk bendk force-pushed the ffi-vtables branch 5 times, most recently from 27ff2f8 to cfdb66b Compare November 1, 2023 18:01
@bendk bendk force-pushed the ffi-vtables branch 2 times, most recently from 3b63929 to da19f25 Compare November 22, 2023 22:03
@bendk bendk force-pushed the ffi-vtables branch 4 times, most recently from 2803505 to 06801af Compare December 11, 2023 17:30
@bendk bendk force-pushed the ffi-vtables branch 4 times, most recently from 4be2eed to 5308aa0 Compare December 13, 2023 01:00
@bendk bendk force-pushed the ffi-vtables branch 2 times, most recently from 6610dfc to 9f3a7d5 Compare December 18, 2023 16:07
@bendk bendk mentioned this pull request Dec 21, 2023
2 tasks
Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a large patch, but it makes a lof of sense and makes callbacks more first-class instead of bolted on. I think you need a CHANGELOG entry for the maintainers of other bindings, and we should avoid landing this until 0.26 is (nearly?) out the door, but it looks great, thanks!

static #internals_ident: ::uniffi::ForeignCallbackInternals = ::uniffi::ForeignCallbackInternals::new();
struct #vtable_type {
#(#vtable_fields)*
uniffi_dec_ref: extern "C" fn(handle: u64),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe uniffi_drop here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dec_ref takes the place of drop now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I thought you were suggesting to add a uniffi_drop field, but now I realize you were probably just saying that the name doesn't make sense which is true. I'll rename this to uniffi_drop.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

less "doesn't make sense" and more "would be more consistent and clearer" but yeah, it was all about the name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with uniffi_free, which matches the name in the foreign code and uniffi_core.

uniffi_bindgen/src/interface/ffi.rs Show resolved Hide resolved
FfiType::Callback(_) => unimplemented!("FFI Callbacks not implemented"),
// Note: references are not really used by the Ruby bindings yet, maybe this should
// change to a different type when they are actually implemented.
FfiType::Reference(_) => ":pointer".to_string(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just unimplemented like the above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the tests fail if it's unimplemented and this is the easiest way to make them pass. I updated the docstring to explain the situation better.

uniffi_bindgen/src/bindings/python/templates/Helpers.py Outdated Show resolved Hide resolved
uniffi_bindgen/src/interface/ffi.rs Outdated Show resolved Hide resolved
fixtures/keywords/kotlin/src/lib.rs Outdated Show resolved Hide resolved
@bendk bendk force-pushed the ffi-vtables branch 3 times, most recently from 835efe0 to 880cbcf Compare January 24, 2024 23:04
@bendk bendk force-pushed the ffi-vtables branch 6 times, most recently from a9054a1 to f9b3be4 Compare January 25, 2024 14:53
This type can be used whenever we need to define a callback functions.
Rather than adding the type by hand, which was quite annoying when I was
doing the async work, we now can just add an item in
`ffi_callback_definitions()`.

This also can help avoid FFI type mismatches, like how the Kotlin
signature used to an i16 instead of an i8 by accident.

Use the new type for the future continuation callback.  I have another
plan for callback interface callbacks.  I didn't touch the foreign
executor callback, I hope to work on that very soon.
Instead of registering a single method to implement a callback
interface, foreign code now registers a VTable.  VTable sounds fancy,
but it's just a repr(C) struct where each field is a callback function.

This gives us some more flexibility with the method signatures.
Before, all arguments were passed using a RustBuffer, but not all FFI
types can be written to a RustBuffer.  In particular, I want to be able
to pass callback function pointers.

This also makes the callback interface FFI closer to the Rust one.  I
wanted to make it match exactly, but it didn't work out.  Unfortunately,
we can't directly return the return value on Python because of an old
ctypes bug (https://bugs.python.org/issue5710). Instead, input an out
param for the return type.  The other main possibility would be to
change `RustBuffer` to be a simple `*mut u8` (mozilla#1779), which would then
be returnable by Python.  However, it seems bad to restrict ourselves
from ever returning a struct in the future. Eventually, we want to stop
using `RustBuffer` for all complex data types and that probably means
using a struct instead in some cases.

Renamed `CALL_PANIC` to `CALL_UNEXPECTED_ERROR` in the foreign bindings
templates.  This matches the name in the Rust code and makes more sense
for foreign trait implementations.

Removed the reexport-scaffolding-macro fixture. I don't think this one
is giving us a lot of value anymore and I don't want to keep updating it
when the FFI changes.
@bendk
Copy link
Contributor Author

bendk commented Jan 25, 2024

I think this one is ready now, officially re-requesting review.

Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢

@@ -304,8 +304,8 @@ open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name }
}

{%- if obj.has_callback_interface() %}
{%- let callback_handler_class = format!("UniffiCallbackInterface{}", name) %}
{%- let callback_handler_obj = format!("uniffiCallbackInterface{}", name) %}
{%- let vtable = obj.vtable().expect("trait interface should have a vtable") %}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this appears unused in this file, but is used in CallbackInterfaceTempl - maybe move the let there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on obj though, so I don't think I can move it. I think of this as ObjectTemplate.kt passing arguments to CallbalkInterfaceImplTemplate.kt

@bendk bendk merged commit 26a2fef into mozilla:main Jan 25, 2024
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants